package com.hboxs.asl.wechat.payment.base;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.hboxs.asl.wechat.MapEntryConverter;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.DomDriver;
import com.thoughtworks.xstream.io.xml.XmlFriendlyNameCoder;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.io.ByteArrayInputStream;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

/**
 * 微信支付数据包<br>
 * 数据结构：key-value<br>
 * 内部结构 TreeMap
 */
public class WxPayData {
    protected Logger logger = Logger.getLogger(WxPayData.class);
    private ObjectMapper mapper = new ObjectMapper();

    /**
     * 采用排序的Dictionary的好处是方便对数据包进行签名，不用再签名之前再做一次排序
     */
    private TreeMap<String, Object> values;

    public WxPayData() {
        values = new TreeMap();
    }

    /**
     * 设置某个字段的值
     *
     * @param key   字段名
     * @param value 字段值
     */
    public void setValue(String key, Object value) {
        if (key != null) {
            values.put(key, value);
        }
    }

    /**
     * 根据字段名获取某个字段的值
     *
     * @param key 字段名
     * @return key对应的字段值
     */
    public Object getValue(String key) {
        return key == null ? null : values.get(key);
    }

    /**
     * 判断某个字段是否已设置
     *
     * @param key 字段名
     * @return 若字段key已被设置，则返回true，否则返回false
     */
    public boolean isSet(String key) {
        return (key != null) && values.containsKey(key);
    }

    /**
     * @return 经转换得到的xml串
     * @throws WxPayException 将Dictionary转成xml
     **/
    public String toXml() throws WxPayException {
        //数据为空时不能转化为xml格式
        if (0 == values.size()) {
            logger.error("WxPayData数据为空!");
            throw new WxPayException("WxPayData数据为空!");
        }

        XStream xStream = new XStream(new DomDriver("UTF-8", new XmlFriendlyNameCoder("-_", "_")));
        xStream.registerConverter(new MapEntryConverter());
        xStream.alias("xml", TreeMap.class);

        return xStream.toXML(values);
    }

    /**
     * @param xml 待转换的xml串
     * @return 经转换得到的Dictionary
     * @throws WxPayException 将xml转为WxPayData对象并返回对象内部的数据
     */
    @SuppressWarnings("unchecked")
    public TreeMap<String, Object> fromXml(String xml) throws WxPayException {
        if (StringUtils.isEmpty(xml)) {
            logger.error("将空的xml串转换为WxPayData不合法!");
            throw new WxPayException("将空的xml串转换为WxPayData不合法!");
        }
        try {
            SAXReader reader = new SAXReader();

            Document document = reader.read(new ByteArrayInputStream(xml.getBytes("UTF-8")));

            // 得到xml根元素
            Element root = document.getRootElement();

            // 得到根元素的所有子节点
            List<Element> elementList = root.elements();

            // 遍历所有子节点
            for (Element e : elementList)
                setValue(e.getName(), e.getText());

        } catch (Exception e) {
            throw new WxPayException(e.getMessage());
        }

        try {
            //2015-06-29 错误是没有签名
            if (!values.get("return_code").equals("SUCCESS")) {
                return values;
            }
            checkSign();//验证签名,不通过会抛异常
        } catch (WxPayException ex) {
            throw new WxPayException(ex.getMessage());
        }

        return values;
    }

    /**
     * Dictionary格式转化成url参数格式
     *
     * @return url格式串, 该串不包含sign字段值
     */
    public String toUrl() throws WxPayException {
        StringBuilder buff = new StringBuilder();

        for (Map.Entry<String, Object> entry : values.entrySet()) {
            System.out.println(entry.getKey()+": " +entry.getValue());
            if (entry.getValue() == null) {
                logger.error("WxPayData内部含有值为null的字段 -- " + entry.getKey());
                throw new WxPayException("WxPayData内部含有值为null的字段 -- " + entry.getKey());
            }

            if (!entry.getKey().equals("sign") && !entry.getValue().toString().equals("")) {
                buff.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
            }
        }

        return buff.substring(0, buff.length() - 1);
    }


    /**
     * @return json串数据
     * Dictionary格式化成Json
     */
    public String toJson() throws JsonProcessingException {
        return mapper.writeValueAsString(values);
    }

    /**
     * values格式化成能在Web页面上显示的结果（因为web页面上不能直接输出xml格式的字符串）
     */
    public String toPrintStr() throws WxPayException {
        String str = "";
        for (Map.Entry<String, Object> entry : values.entrySet()) {
            if (entry.getValue() == null) {
                logger.error("WxPayData内部含有值为null的字段!");
                throw new WxPayException("WxPayData内部含有值为null的字段!");
            }

            str += String.format("%s=%s<br>", entry.getKey(), entry.getValue().toString());
        }
        logger.debug("Print in Web Page : " + str);
        return str;
    }

    /**
     * @return 签名, sign字段不参加签名
     * 生成签名，详见签名生成算法
     */
    public String makeSign() throws WxPayException {
        //转url格式
        String str = toUrl();

        //在String后加入API KEY
        str += "&key=" + WxPayConfig.KEY;

        logger.debug("Sign String:" + str);
        //MD5加密
        str = DigestUtils.md5Hex(str);
        logger.debug("MD5 Hash:" + str);

        //所有字符转为大写
        return str.toUpperCase();
    }

    /**
     * 检测签名是否正确
     * 正确返回true，错误抛异常
     */
    public boolean checkSign() throws WxPayException {
        //如果没有设置签名，则跳过检测
        if (!isSet("sign")) {
            logger.error("WxPayData签名存在但不合法!");
            throw new WxPayException("WxPayData签名存在但不合法!");
        }
        //如果设置了签名但是签名为空，则抛异常
        else if (StringUtils.isEmpty((String) getValue("sign"))) {
            logger.error("WxPayData签名存在但不合法!");
            throw new WxPayException("WxPayData签名存在但不合法!");
        }

        //获取接收到的签名
        String return_sign = getValue("sign").toString();

        //在本地计算新的签名
        String cal_sign = makeSign();
        if (StringUtils.equals(cal_sign, return_sign)) {
            return true;
        }

        logger.error("WxPayData签名验证错误!");
        throw new WxPayException("WxPayData签名验证错误!");
    }

    /**
     * 获取Dictionary
     */
    public TreeMap<String, Object> getValues() {
        return values;
    }

    public String makeSha1Sign() throws WxPayException {
        logger.debug("SHA1 Encrypt String:"+toUrl());
        return DigestUtils.shaHex(toUrl());
    }

    public Object remove(String key){
        Object o = null;
        if(isSet(key)){
            o = values.remove(key);
        }

        return o;
    }

}
